#include	"ImageTGA.h"
#include	<OpenGL/GLException.h>
#include	<fstream>
#include	<iostream>
#include	<memory>
#include	<windows.h>
#include	<gl/gl.h>

ImageTGA::ImageTGA()
{
	m_id = 0;
	m_colorMap = 0;
	m_imageData = 0;
	Cleanup();
}

ImageTGA::ImageTGA(const char * file)
{
	m_id = 0;
	m_colorMap = 0;
	m_imageData = 0;
	Cleanup();
	Load(file);
	
}

ImageTGA::~ImageTGA()
{
	Cleanup();
}

void ImageTGA::CleanupInformation()
{
	m_idLength = 0;
	m_colorMapType = 0;
	m_imageType = 0;
	m_colorMapFirstEntry = 0;
	m_colorMapLength = 0;
	m_colorMapBitsPerEntry = 0;
	m_imageOriginX = 0;
	m_imageOriginY = 0;
	m_imageWidth = 0;
	m_imageHeight = 0;
	m_bitsPerPixel = 0;
	m_imageDescriptor = 0;
	m_headerLoaded = false;
}

void ImageTGA::CleanupData()
{
	delete[] m_id;
	m_id = 0;
	m_idLoaded = false;
	delete[] m_colorMap;
	m_colorMap = 0;
	m_colorMapLoaded = false;
	delete[] m_imageData;
	m_imageData = 0;
	m_imageDataLoaded = false;
}

void ImageTGA::Cleanup()
{
	CleanupData();
	CleanupInformation();
}

void ImageTGA::Load(const unsigned char * buffer, unsigned long bufferSize)
{
	try
	{
		ReadHeader(buffer, bufferSize);
		ReadID(buffer, bufferSize);
		ReadColorMap(buffer, bufferSize);
		ReadImageData(buffer, bufferSize);
		FixColors();
		Flip();
	}
	catch(...)
	{
		Cleanup();
		throw;
	}
}

void ImageTGA::Load(const char * filename)
{
	std::ifstream file(filename, std::ios::in | std::ios::binary);
	if(!file)
	{
		throw TextureException("File not found");
	}
	file.seekg(0, std::ios::end);
	unsigned long fileSize = file.tellg();
	file.seekg(0, std::ios::beg);
	unsigned char * fileContent = new unsigned char[fileSize];
	file.read(reinterpret_cast< char * >(fileContent), fileSize);
	file.close();
	Load(fileContent, fileSize);
	delete[] fileContent;
	
}

bool ImageTGA::Valid() const
{
	return m_headerLoaded && m_idLoaded && m_colorMapLoaded && m_imageDataLoaded;
}

void ImageTGA::RegisterInOpenGL(unsigned int name) const
{
	GLenum code = GL_NO_ERROR;
	glBindTexture(GL_TEXTURE_2D, name);
	code = glGetError();
	if(code != GL_NO_ERROR)
	{
		throw GLException(code);
	}
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	code = glGetError();
	if(code != GL_NO_ERROR)
	{
		throw GLException(code);
	}
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	code = glGetError();
	if(code != GL_NO_ERROR)
	{
		throw GLException(code);
	}
	switch(m_imageType)
	{
	case 0:
	case 1:
	case 3:
	case 9:
	case 11:
		{
			throw TextureException("Unsupported texture format");
		}
		break;
	case 2:
	case 10:
		{
			unsigned char attribBits = m_imageDescriptor & 0x0F;
			unsigned int bytesPerPixel = (attribBits > 0) ? GL_RGBA : GL_RGB;
			glTexImage2D(GL_TEXTURE_2D, 0, bytesPerPixel,
				m_imageWidth, m_imageHeight, 0, bytesPerPixel, GL_UNSIGNED_BYTE, m_imageData);
			code = glGetError();
			if(code != GL_NO_ERROR)
			{
				throw GLException(code);
			}
		}
		break;
	}
}

void ImageTGA::ReadHeader(const unsigned char * buffer, unsigned long bufferSize)
{
	if((buffer == 0) || (bufferSize < 18))
	{
		throw TextureException("Invalid buffer");
	}
	Cleanup();
	// 1.
	m_idLength = buffer[0];
	// 2. valid values are: 0, 1
	m_colorMapType = buffer[1] & 0x01;
	if(m_colorMapType > 1)
	{
		throw TextureException("Unsuported color map type (Field 2)");
	}
	// 3. valid values are: 0, 1, 2, 3, 9, 10, 11
	m_imageType = buffer[2];
	if(((3 < m_imageType) && (m_imageType < 9)) || (11 < m_imageType))
	{
		throw TextureException("Invalid image type (Field 3)");
	}
	else if((m_imageType != 2) && (m_imageType != 10))
	{
		throw TextureException("Unsupported image type (Field 3)");
	}
	// 4.1.
	m_colorMapFirstEntry = buffer[4] * 256 + buffer[3];
	// 4.2.
	m_colorMapLength = buffer[6] * 256 + buffer[5];
	// 4.3. valid values are: 0, 16, 24, 32
	m_colorMapBitsPerEntry = buffer[7];
	if((m_colorMapBitsPerEntry != 0)
	&& (m_colorMapBitsPerEntry != 16)
	&& (m_colorMapBitsPerEntry != 24)
	&& (m_colorMapBitsPerEntry != 32))
	{
		throw TextureException("Unsuported color map bit depth (Field 4.3)");
	}
	// 5.1.
	m_imageOriginX = buffer[9] * 256 + buffer[8];
	// 5.2.
	m_imageOriginY = buffer[11] * 256 + buffer[10];
	// 5.3.
	m_imageWidth = buffer[13] * 256 + buffer[12];
	// 5.4.
	m_imageHeight = buffer[15] * 256 + buffer[14];
	// 5.5. valid values are: 8, 16, 24, 32
	m_bitsPerPixel = buffer[16];
	if(((m_bitsPerPixel % 8) != 0) || (m_bitsPerPixel > 32))
	{
		throw TextureException("Unsuported image bits per pixel number (Field 5.5)");
	}
	// 5.6.
	m_imageDescriptor = buffer[17];
	if((m_imageDescriptor & 0x0F) > 8)
	{
		throw TextureException("Unsuported alpha channel bit length (Field 5.6)");
	}
	unsigned char attribBits = 0;
	if(m_colorMapType == 0)
	{
		unsigned char bitsPerColor = (m_bitsPerPixel / 3);
		if(bitsPerColor > 8)
		{
			bitsPerColor = 8;
		}
		attribBits = (m_bitsPerPixel - (bitsPerColor * 3));
	}
	else
	{
		unsigned char bitsPerColor = (m_colorMapBitsPerEntry / 3);
		if(bitsPerColor > 8)
		{
			bitsPerColor = 8;
		}
		attribBits = (m_colorMapBitsPerEntry - (bitsPerColor * 3));
	}
	if((m_colorMapType == 0) && ((m_imageDescriptor & 0x0F) > attribBits))
	{
		throw TextureException("Alpha channel bit length is greater than attrib bits (Field 5.6)");
	}
	if((m_imageDescriptor & 0xC0) != 0)
	{
		throw TextureException("Corrupted image descriptor (Field 5.6)");
	}
	m_headerLoaded = true;
}

void ImageTGA::ReadID(const unsigned char * buffer, unsigned long bufferSize)
{
	if(m_idLength > 0)
	{
		if(bufferSize < (18 + unsigned long(m_idLength)))
		{
			throw TextureException("Truncated file (Field 6)");
		}
		m_id = new unsigned char[m_idLength];
		memcpy(m_id, buffer + 18, m_idLength);
	}
	else
	{
		m_id = 0;
	}
	m_idLoaded = true;
}

void ImageTGA::ReadColorMap(const unsigned char * buffer, unsigned long bufferSize)
{
	if(m_colorMapType == 1)
	{
		if((m_imageType != 1) && (m_imageType != 9))
		{
			throw TextureException("Conflicting header information: \'color map type\' and \'image type\' fields (Field 7)");
		}
		unsigned long start = 18 + m_idLength;
		unsigned long length = m_colorMapLength * (m_colorMapBitsPerEntry / 8);
		if(bufferSize < (start + length))
		{
			throw TextureException("Truncated file (Field 7)");
		}
		m_colorMap = new unsigned char[length];
		memcpy(m_colorMap, buffer + start, length);
	}
	else
	{
		m_colorMapFirstEntry = 0;
		m_colorMapLength = 0;
		m_colorMapBitsPerEntry = 0;
		m_colorMap = 0;
	}
	m_colorMapLoaded = true;
}

void ImageTGA::ReadImageData(const unsigned char * buffer, unsigned long bufferSize)
{
	switch(m_imageType)
	{
	case 0:// no image data
		{
			m_imageData = 0;
		}
		break;
	case 1:// raw, color-map
	case 2:// raw, true-color
	case 3:// raw, black-and-white
		{
			unsigned long start = 18 + m_idLength + m_colorMapLength * (m_colorMapBitsPerEntry / 8);
			unsigned long length = m_imageWidth * m_imageHeight * (m_bitsPerPixel / 8);
			if(bufferSize < (start + length))
			{
				throw TextureException("Truncated file (Field 8)");
			}
			m_imageData = new unsigned char[length];
			memcpy(m_imageData, buffer + start, length);
		}
		break;
	case 9:// rle-compressed, color-map
	case 10:// rle-compressed, true-color
	case 11:// rle-compressed, black-and-white
		{
			const unsigned long start = 18 + m_idLength + m_colorMapLength * (m_colorMapBitsPerEntry / 8);
			const unsigned char pixelSize = (m_bitsPerPixel / 8);
			const unsigned long totalPixels = m_imageWidth * m_imageHeight;
			const unsigned long totalBytes = totalPixels * pixelSize;
			m_imageData = new unsigned char[totalBytes];
			const unsigned char * buf = buffer + start;
			unsigned long pixelsAssigned = 0;
			unsigned char packetHeader = 0;
			unsigned long pixelsToRead = 0;
			while((pixelsAssigned < totalPixels) && (buf < (buffer + bufferSize)))
			{
				packetHeader = *buf;
				++buf;
				pixelsToRead = (packetHeader & 0x7F) + 1;
				if((packetHeader & 0x80) == 0)
				{
					memcpy(m_imageData + pixelsAssigned * pixelSize, buf, pixelsToRead * pixelSize);
					buf += pixelsToRead * pixelSize;
				}
				else
				{
					for(unsigned long i = 0; i < pixelsToRead; ++i)
					{
						memcpy(m_imageData + pixelsAssigned * pixelSize + i * pixelSize, buf, pixelSize);
					}
					buf += pixelSize;
				}
				pixelsAssigned += pixelsToRead;
			}
			if(pixelsAssigned < totalPixels)
			{
				throw TextureException("Truncated file (Field 8)");
			}
		}
		break;
	}
	m_imageDataLoaded = true;
}

void ImageTGA::FixColors()
{
	const unsigned long pixelCount = unsigned long(m_imageWidth) * m_imageHeight;
	for(unsigned long i = 0; i < pixelCount; i += 1)
	{
		unsigned long pixelOffset = i * (m_bitsPerPixel / 8);
		unsigned char temporary = m_imageData[pixelOffset];
		m_imageData[pixelOffset] = m_imageData[pixelOffset + 2];
		m_imageData[pixelOffset + 2] = temporary;
	}
}

void ImageTGA::Flip()
{
	const bool flipHorizontal = (m_imageDescriptor & 0x10) != 0;
	const bool flipVertical = (m_imageDescriptor & 0x20) != 0;
	const unsigned char bytesPerPixel = (m_bitsPerPixel / 8);
	unsigned char buff[8] = {0};
	if(flipHorizontal)
	{
		for(unsigned int y = 0; y < unsigned long(m_imageHeight); ++y)
		{
			for(unsigned int x = 0; x < unsigned long(m_imageWidth / 2); ++x)
			{
				unsigned long pixelOffset1 = ((y * m_imageWidth) + x) * bytesPerPixel;
				unsigned long pixelOffset2 = ((y * m_imageWidth) + (m_imageWidth - x - 1)) * bytesPerPixel;
				std::memcpy(buff, m_imageData + pixelOffset1, bytesPerPixel);
				std::memcpy(m_imageData + pixelOffset1, m_imageData + pixelOffset2, bytesPerPixel);
				std::memcpy(m_imageData + pixelOffset1, buff, bytesPerPixel);
			}
		}
	}
	if(flipVertical)
	{
		for(unsigned int x = 0; x < unsigned long(m_imageWidth); ++x)
		{
			for(unsigned int y = 0; y < unsigned long(m_imageHeight / 2); ++y)
			{
				unsigned long pixelOffset1 = ((y * m_imageWidth) + x) * bytesPerPixel;
				unsigned long pixelOffset2 = (((y - m_imageHeight - 1) * m_imageWidth) + x) * bytesPerPixel;
				std::memcpy(buff, m_imageData + pixelOffset1, bytesPerPixel);
				std::memcpy(m_imageData + pixelOffset1, m_imageData + pixelOffset2, bytesPerPixel);
				std::memcpy(m_imageData + pixelOffset1, buff, bytesPerPixel);
			}
		}
	}
}

